<!doctype html>
<html>
	<head>
		<style>
			body {
				--bg-1: hsl(0, 0%, 100%);
				--bg-2: hsl(206, 20%, 90%);
				--bg-3: hsl(206, 20%, 80%);
				--fg-1: hsl(0, 0%, 13%);
				--fg-2: hsl(0, 0%, 20%);
				--fg-2: hsl(0, 0%, 30%);
				--link: hsl(208, 77%, 47%);
				--link-hover: hsl(208, 77%, 55%);
				--link-active: hsl(208, 77%, 40%);
				--border-radius: 4px;
				--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
					'Open Sans', 'Helvetica Neue', sans-serif;
				--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
					'DejaVu Sans Mono', monospace;
				color-scheme: light;
				background: var(--bg-1);
				color: var(--fg-1);
				font-family: var(--font);
				line-height: 1.5;
				margin: 1rem;
				height: calc(100vh - 2rem);
				accent-color: var(--hover) !important;
			}

			a {
				color: var(--link);
			}

			a:hover {
				color: var(--link-hover);
			}

			a:active {
				color: var(--link-active);
			}

			code {
				background: var(--bg-2);
				font-family: var(--font-mono);
				font-size: 0.9em;
				padding: 0.15rem 0.3rem;
				border-radius: var(--border-radius);
			}

			ul.todos {
				padding: 0;
			}

			body.dark {
				color-scheme: dark;
				--bg-1: hsl(0, 0%, 18%);
				--bg-2: hsl(0, 0%, 30%);
				--bg-3: hsl(0, 0%, 40%);
				--fg-1: hsl(0, 0%, 90%);
				--fg-2: hsl(0, 0%, 70%);
				--fg-3: hsl(0, 0%, 60%);
				--link: hsl(206, 96%, 72%);
				--link-hover: hsl(206, 96%, 78%);
				--link-active: hsl(206, 96%, 64%);
			}
		</style>

		<script>
			(function () {
				function handle_message(ev) {
					let { action, cmd_id } = ev.data;
					const send_message = (payload) => parent.postMessage({ ...payload }, ev.origin);
					const send_reply = (payload) => send_message({ ...payload, cmd_id });
					const send_ok = () => send_reply({ action: 'cmd_ok' });
					const send_error = (message, stack) =>
						send_reply({ action: 'cmd_error', message, stack });

					if (action === 'set_theme') {
						const { theme } = ev.data.args;

						document.body.classList.toggle('dark', theme === 'dark');
						send_ok();
					}

					if (action === 'eval') {
						try {
							const { script } = ev.data.args;
							eval(script);
							send_ok();
						} catch (e) {
							send_error(e.message, e.stack);
						}
					}

					if (action === 'catch_clicks') {
						try {
							const top_origin = ev.origin;
							document.body.addEventListener('click', (event) => {
								if (event.which !== 1) return;
								if (event.metaKey || event.ctrlKey || event.shiftKey) return;
								if (event.defaultPrevented) return;

								// ensure target is a link
								let el = event.target;
								while (el && el.nodeName !== 'A') el = el.parentNode;
								if (!el || el.nodeName !== 'A') return;

								if (
									el.hasAttribute('download') ||
									el.getAttribute('rel') === 'external' ||
									el.target
								)
									return;

								event.preventDefault();

								if (el.href.startsWith(top_origin)) {
									const url = new URL(el.href);
									if (url.hash[0] === '#') {
										window.location.hash = url.hash;
										return;
									}
								}

								window.open(el.href, '_blank');
							});
							send_ok();
						} catch (e) {
							send_error(e.message, e.stack);
						}
					}
				}

				window.addEventListener('message', handle_message, false);

				window.onerror = function (msg, url, lineNo, columnNo, error) {
					parent.postMessage({ action: 'error', value: error }, '*');
				};

				window.addEventListener('unhandledrejection', (event) => {
					parent.postMessage({ action: 'unhandledrejection', value: event.reason }, '*');
				});
			}).call(this);

			let previous = { level: null, args: null };

			['clear', 'log', 'info', 'dir', 'warn', 'error', 'table'].forEach((level) => {
				const original = console[level];
				console[level] = (...args) => {
					const stringifiedArgs = stringify(args);
					if (previous.level === level && previous.args && previous.args === stringifiedArgs) {
						parent.postMessage({ action: 'console', level, duplicate: true }, '*');
					} else {
						previous = { level, args: stringifiedArgs };

						try {
							parent.postMessage({ action: 'console', level, args }, '*');
						} catch (err) {
							parent.postMessage({ action: 'console', level: 'unclonable' }, '*');
						}
					}

					original(...args);
				};
			});

			[
				{ method: 'group', action: 'console_group' },
				{ method: 'groupEnd', action: 'console_group_end' },
				{ method: 'groupCollapsed', action: 'console_group_collapsed' }
			].forEach((group_action) => {
				const original = console[group_action.method];
				console[group_action.method] = (label) => {
					parent.postMessage({ action: group_action.action, label }, '*');

					original(label);
				};
			});

			const timers = new Map();
			const original_time = console.time;
			const original_timelog = console.timeLog;
			const original_timeend = console.timeEnd;

			console.time = (label = 'default') => {
				original_time(label);
				timers.set(label, performance.now());
			};
			console.timeLog = (label = 'default') => {
				original_timelog(label);
				const now = performance.now();
				if (timers.has(label)) {
					parent.postMessage(
						{
							action: 'console',
							level: 'system-log',
							args: [`${label}: ${now - timers.get(label)}ms`]
						},
						'*'
					);
				} else {
					parent.postMessage(
						{ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] },
						'*'
					);
				}
			};
			console.timeEnd = (label = 'default') => {
				original_timeend(label);
				const now = performance.now();
				if (timers.has(label)) {
					parent.postMessage(
						{
							action: 'console',
							level: 'system-log',
							args: [`${label}: ${now - timers.get(label)}ms`]
						},
						'*'
					);
				} else {
					parent.postMessage(
						{ action: 'console', level: 'system-warn', args: [`Timer '${label}' does not exist`] },
						'*'
					);
				}
				timers.delete(label);
			};

			const original_assert = console.assert;
			console.assert = (condition, ...args) => {
				if (condition) {
					const stack = new Error().stack;
					parent.postMessage({ action: 'console', level: 'assert', args, stack }, '*');
				}
				original_assert(condition, ...args);
			};

			const counter = new Map();
			const original_count = console.count;
			const original_countreset = console.countReset;

			console.count = (label = 'default') => {
				counter.set(label, (counter.get(label) || 0) + 1);
				parent.postMessage(
					{ action: 'console', level: 'system-log', args: `${label}: ${counter.get(label)}` },
					'*'
				);
				original_count(label);
			};

			console.countReset = (label = 'default') => {
				if (counter.has(label)) {
					counter.set(label, 0);
				} else {
					parent.postMessage(
						{
							action: 'console',
							level: 'system-warn',
							args: `Count for '${label}' does not exist`
						},
						'*'
					);
				}
				original_countreset(label);
			};

			const original_trace = console.trace;

			console.trace = (...args) => {
				const stack = new Error().stack;
				parent.postMessage({ action: 'console', level: 'trace', args, stack }, '*');
				original_trace(...args);
			};

			function stringify(args) {
				try {
					return JSON.stringify(args);
				} catch (error) {
					return null;
				}
			}
		</script>
	</head>
	<body></body>
</html>
